#include "../../lib/plane.h"
#include "../../lib/droplet.h"
#include "../../lib/graphics.h"
#include "../../lib/shading.h"
#include "../../lib/vector/vec.h"
#include "../../lib/minmax_int.h"
#include "../../lib/time_xplat.h"

#define STB_IMAGE_IMPLEMENTATION
#include <stb/stb_image.h>
#include <math.h>
#include <time.h>
#include <stdbool.h>
#include <assert.h>


// 0 for unlimited frame rate, otherwise limited to the given value
static unsigned int const MAX_FRAME_RATE = 60;
// 0 to not use a test scene, 1 or above to use the numbered test scene
static int const USE_TEST_SCENE = 0;
// 0 to disable generating new droplets, 1 to enable it
static int const ENABLE_DROPLET_GENERATOR = 1;
static float const PLANE_WIDTH = 1000.0;
static float const PLANE_HEIGHT = 1000.0;
// Minimum width a droplet must be to move
static float const PLANE_FRICTION = 5.0;
// Changes the speed droplets fall at
static float const DROPLET_SPEED_MULTIPLIER = 0.5;

static char const * BACKGROUND_IMAGE_FILE = "background.png";
static char const * SHADER_DROPLET_VERTEX_FILE = "droplet_vertex.glsl";
static char const * SHADER_DROPLET_FRAGMENT_FILE = "droplet_fragment.glsl";
static char const * SHADER_BACKGROUND_VERTEX_FILE = "background_vertex.glsl";
static char const * SHADER_BACKGROUND_FRAGMENT_FILE = \
		"background_fragment.glsl";

char * source_dir;


// Function declarations
static Droplet droplet_create(float max_x, float max_y, float max_radius);
static void rand_add_droplet(float prob, float max_x, float max_y,
		float max_radius, Plane * p);
static char * get_relative_path(char const * const file_name);
static char * read_file(char const * const file);
static GLuint render_background(GLuint background_shader_id);
static GLuint create_draw_background_vao(GLuint background_texture_id,
		GLuint * buffer_ids, size_t buffer_ids_length);
static void draw_background(GLuint vao_id, GLuint background_shader_id);
static GLuint create_draw_droplets_vao(Plane * p, GLuint * buffer_ids,
		size_t buffer_ids_length);
static void draw_droplets(GLuint vao_id, GLuint droplet_shader_id,
		size_t droplet_count);
static void draw(Plane * p, GLuint draw_background_vao_id,
		GLuint droplet_shader_id, GLuint background_shader_id,
		SDL_Window * window);
static void move_droplets(double time_delta, Plane * p);
static void merge_overlapping_droplets(Plane * p);
static void loop(double time_delta, int max_new_droplets_per_iter, Plane * p,
		SDL_Window * window, GLuint draw_background_vao_id,
		GLuint droplet_shader_id, GLuint background_shader_id);
static void create_initial_droplets(Plane * p);
static void create_testing_droplets(Plane * p, int scene_number);


static Droplet droplet_create(float max_x, float max_y, float max_radius) {
	Droplet new_droplet;
	new_droplet.x = rand() * (max_x / RAND_MAX);
	new_droplet.y = rand() * (max_y / RAND_MAX);
	new_droplet.radius = rand() * (max_radius / RAND_MAX);
	return new_droplet;
}


static void rand_add_droplet(float prob, float max_x, float max_y,
		float max_radius, Plane * p) {
	if (rand() / RAND_MAX < prob) {
		vector_add(&(p->droplets), droplet_create(max_x, max_y, max_radius));
	}
}


static char * get_relative_path(char const * const file_name) {
	/*
	Returns:
		The path to `file_name` relative to the directory the program was
		started from.
		
		The returned value must be freed
	*/
	unsigned int char_count = strlen(source_dir) + strlen(file_name) + 2;
	char * relative_path = calloc(char_count, sizeof(*relative_path));
	strncpy(relative_path, source_dir, strlen(source_dir));
	strcat(relative_path, "/");
	strncat(relative_path, file_name, strlen(file_name));
	return relative_path;
}


static char * read_file(char const * const file) {
	int err = 0;
	FILE * f = fopen(file, "r");
	assert(f != NULL);
	err = fseek(f, 0, SEEK_END);
	assert(err == 0);
	long file_size = ftell(f);
	assert(file_size != -1);
	err = fseek(f, 0, SEEK_SET);
	assert(err == 0);
	char * text = malloc(file_size + 1);
	assert(text != NULL);
	fread(text, 1, file_size, f);
	text[file_size] = '\0';
	fclose(f);
	return text;
}


static GLuint render_background(GLuint background_shader_id) {
	int image_width = 0;
	int image_height = 0;
	int components_per_pixel = 0;
	char * background_image_rel_path = get_relative_path(BACKGROUND_IMAGE_FILE);
	unsigned char * image_data = stbi_load(background_image_rel_path,
			&image_width, &image_height, &components_per_pixel, 0);
	GLfloat vertexes[] = {
		// Vertex coords	// Texture coords
		-1.0f,  1.0f,		0.0f, 0.0f,
		 1.0f,  1.0f,		1.0f, 0.0f,
		 1.0f, -1.0f,		1.0f, 1.0f,
		-1.0f, -1.0f,		0.0f, 1.0f,
	};
	GLuint indexes[] = {
		0, 1, 3,
		1, 2, 3,
	};
	
	// Create the texture that will store the rendered frame
	GLuint rendered_texture_id;
	glGenTextures(1, &rendered_texture_id);
	glBindTexture(GL_TEXTURE_2D, rendered_texture_id);
	// Initialize the texture, omitting image data
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, PLANE_WIDTH, PLANE_HEIGHT, 0, GL_RGB,
			GL_UNSIGNED_BYTE, NULL);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	
	// Create the texture that will store the background image
	GLuint texture_id;
	glGenTextures(1, &texture_id);
	glBindTexture(GL_TEXTURE_2D, texture_id);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image_width, image_height, 0,
			GL_RGBA, GL_UNSIGNED_BYTE, image_data);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	
	// Create the frame buffer
	GLuint frame_buffer_id;
	glGenFramebuffers(1, &frame_buffer_id);
	glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer_id);
	glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
			rendered_texture_id, 0);
	GLenum draw_buffers[1] = {GL_COLOR_ATTACHMENT0};
	glDrawBuffers(sizeof(draw_buffers) / sizeof(draw_buffers[0]), draw_buffers);
	assert(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE);
	
	// Create the vertex buffer object
	GLuint vertex_buffer_id;
	glGenBuffers(1, &vertex_buffer_id);
	glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_id);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertexes[0]) * sizeof(vertexes),
			vertexes, GL_STATIC_DRAW);
	
	// Create the element buffer object
	GLuint element_buffer_id;
	glGenBuffers(1, &element_buffer_id);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, element_buffer_id);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indexes[0]) * sizeof(indexes),
			indexes, GL_STATIC_DRAW);
	
	// Set the position attribute
	glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(vertexes[0]) * 4,
			(void *) 0);
	glEnableVertexAttribArray(0);
	// Set the texture coordinates attribute
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(vertexes[0]) * 4,
			(void *) (sizeof(vertexes[0]) * 2));
	glEnableVertexAttribArray(1);
	
	glUseProgram(background_shader_id);
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
	
	// Return to drawing to the screen
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	
	// Free memory
	stbi_image_free(image_data);
	glDeleteTextures(1, &texture_id);
	glDeleteBuffers(1, &element_buffer_id);
	glDeleteBuffers(1, &vertex_buffer_id);
	glDeleteFramebuffers(1, &frame_buffer_id);
	
	return rendered_texture_id;
}


static GLuint create_draw_background_vao(GLuint background_texture_id,
		GLuint * buffer_ids, size_t buffer_ids_length) {
	/*
	Args:
		[in] background_texture_id: ID created by calling `glGenTextures()`.
			Should store the texture that will be displayed as the background
		[out] buffer_ids: array that will store the buffers that should be
			deleted when the program ends
		[in] buffer_ids_length: the number of elements `buffer_ids` can store.
			Should be at least 2
	
	Returns:
		The ID of the vertex array object which should be used to draw the
		background
	*/
	assert(buffer_ids_length >= 2);
	
	GLfloat vertexes[] = {
		// Vertex coords	// Texture coords
		-1.0f,  1.0f,		0.0f, 1.0f,
		 1.0f,  1.0f,		1.0f, 1.0f,
		 1.0f, -1.0f,		1.0f, 0.0f,
		-1.0f, -1.0f,		0.0f, 0.0f,
	};
	GLuint indexes[] = {
		0, 1, 3,
		1, 2, 3,
	};
	
	// Create the vertex array object
	GLuint vertex_object_id;
	glGenVertexArrays(1, &vertex_object_id);
	glBindVertexArray(vertex_object_id);
	
	// Create the vertex buffer object
	glGenBuffers(1, &(buffer_ids[0]));
	glBindBuffer(GL_ARRAY_BUFFER, buffer_ids[0]);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertexes[0]) * sizeof(vertexes),
			vertexes, GL_STATIC_DRAW);
	
	// Create the element buffer object
	glGenBuffers(1, &(buffer_ids[1]));
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer_ids[1]);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indexes[0]) * sizeof(indexes),
			indexes, GL_STATIC_DRAW);
	
	// Set the position attribute
	glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(vertexes[0]) * 4,
			(void *) 0);
	glEnableVertexAttribArray(0);
	// Set the texture coordinates attribute
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(vertexes[0]) * 4,
			(void *) (sizeof(vertexes[0]) * 2));
	glEnableVertexAttribArray(1);
	
	// Bind the background texture
	glBindTexture(GL_TEXTURE_2D, background_texture_id);
	
	// Bind default VAO so that no other changes are accidentally made to the
	// current VAO
	glBindVertexArray(0);
	
	return vertex_object_id;
}


static void draw_background(GLuint vao_id, GLuint background_shader_id) {
	glBindVertexArray(vao_id);
	glUseProgram(background_shader_id);
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
	
	// Bind default VAO so that no other changes are accidentally made to the
	// current VAO
	glBindVertexArray(0);
}


static GLuint create_draw_droplets_vao(Plane * p, GLuint * buffer_ids,
		size_t buffer_ids_length) {
	/*
	Args:
		[in] p: the Plane object
		[out] buffer_ids: array that will store the buffers that should be
			deleted when the program ends
		[in] buffer_ids_length: the number of elements `buffer_ids` can store.
			Should be at least 1
	
	Returns:
		The ID of the vertex array object which should be used to draw the
		background
	*/
	assert(buffer_ids_length >= 1);
	
	// Calculate the vertexes to draw the droplets
	GLfloat * vertexes = NULL;
	size_t vertexes_size = \
			sizeof(*vertexes)
			* vector_size(p->droplets)
			* VERTEXES_PER_CIRCLE
			* 3;
	vertexes = malloc(vertexes_size);
	create_circles(vertexes, p->droplets, vector_size(p->droplets), p->width,
			p->height);
	
	// Create the vertex array object
	GLuint vertex_object_id;
	glGenVertexArrays(1, &vertex_object_id);
	glBindVertexArray(vertex_object_id);
	
	// Create a buffer object
	glGenBuffers(1, &(buffer_ids[0]));
	glBindBuffer(GL_ARRAY_BUFFER, buffer_ids[0]);
	// Create and initialize the data store of the buffer
	glBufferData(GL_ARRAY_BUFFER, vertexes_size, vertexes, GL_STATIC_DRAW);
	
	glEnableVertexAttribArray(1);
	
	// Bind default VAO so that no other changes are accidentally made to the
	// current VAO
	glBindVertexArray(0);
	
	free(vertexes);
	
	return vertex_object_id;
}


static void draw_droplets(GLuint vao_id, GLuint droplet_shader_id,
		size_t droplet_count) {
	glBindVertexArray(vao_id);
	glUseProgram(droplet_shader_id);
	// Set the screen size attribute
	glVertexAttrib2f(0, PLANE_WIDTH, PLANE_HEIGHT);
	// Set the vertex position attribute
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, (void*) 0);
	
	// Draw the circles
	for (size_t circle = 0; circle < droplet_count; circle++) {
		glDrawArrays(GL_TRIANGLE_FAN, circle * VERTEXES_PER_CIRCLE,
				VERTEXES_PER_CIRCLE);
	}
	
	// Bind default VAO so that no other changes are accidentally made to the
	// current VAO
	glBindVertexArray(0);
}


static void draw(Plane * p, GLuint draw_background_vao_id, 
		GLuint droplet_shader_id, GLuint background_shader_id,
		SDL_Window * window) {
	glClear(GL_COLOR_BUFFER_BIT);
	
	// Draw the background texture
	draw_background(draw_background_vao_id, background_shader_id);
	
	// Draw the droplets
	GLuint droplets_buffer_ids[1];
	size_t droplets_buffer_ids_length = \
			sizeof(droplets_buffer_ids) / sizeof(droplets_buffer_ids[0]);
	GLuint draw_droplets_vao_id = create_draw_droplets_vao(p,
			droplets_buffer_ids, droplets_buffer_ids_length);
	draw_droplets(draw_droplets_vao_id, droplet_shader_id,
			vector_size(p->droplets));
	
	SDL_GL_SwapWindow(window);
	
	glDeleteBuffers(droplets_buffer_ids_length, droplets_buffer_ids);
	glDeleteVertexArrays(1, &draw_droplets_vao_id);
}


static void move_droplets(double time_delta, Plane * p) {
	for (unsigned int i = 0; i < vector_size(p->droplets); i++) {
		// Move droplet if it overcomes friction
		p->droplets[i].y += \
				M_PI * pow(p->droplets[i].radius, 2.0f)
				* DROPLET_SPEED_MULTIPLIER
				* (p->droplets[i].radius > p->friction)
				* time_delta;
	}
}


static void merge_overlapping_droplets(Plane * p) {
	if (vector_size(p->droplets) < 2) {
		return;
	}
	vec_uint droplets_to_erase = vector_create();
	for (unsigned int i = 0; i < vector_size(p->droplets) - 1; i++) {
		for (unsigned int j = i + 1; j < vector_size(p->droplets); j++) {
			Droplet * dropi = &(p->droplets[i]);
			Droplet * dropj = &(p->droplets[j]);
			float displacement[2] = {dropj->x - dropi->x, dropj->y - dropi->y};
			float distance = sqrt(
				pow(displacement[0], 2) + pow(displacement[1], 2)
			);
			float radius_sum = dropi->radius + dropj->radius;
			float size_ratio = dropi->radius / (dropi->radius + dropj->radius);
			if (distance < radius_sum) {
				float max_radius = fmaxf(dropi->radius, dropj->radius);
				float min_radius = fminf(dropi->radius, dropj->radius);
				dropi->radius = max_radius + min_radius / max_radius;
				dropi->x += displacement[0] * size_ratio;
				// Get the max to prevent droplet from moving upwards
				dropi->y += fmaxf(0.0f, displacement[0] * size_ratio);
				vector_add(&droplets_to_erase, j);
			}
		}
		// Erase merged droplets
		int last_idx = vector_size(droplets_to_erase) - 1;
		while (last_idx > -1) {
			vector_remove(&(p->droplets), droplets_to_erase[last_idx]);
			last_idx--;
		}
		vector_erase(&droplets_to_erase, 0, vector_size(droplets_to_erase));
	}
	vector_free(droplets_to_erase);
}


static void loop(
		double time_delta,
		int max_new_droplets_per_iter,
		Plane * p,
		SDL_Window * window,
		GLuint draw_background_vao_id,
		GLuint droplet_shader_id,
		GLuint background_shader_id) {
	if (vector_size(p->droplets) > 0) {
		move_droplets(time_delta, p);
		
		// Delete droplets that are off the screen
		for (size_t i = 0; i < vector_size(p->droplets); i++) {
			if (p->droplets[i].y - p->droplets[i].radius > p->height) {
				vector_remove(&(p->droplets), i);
			}
		}
	}
	
	if (ENABLE_DROPLET_GENERATOR) {
		// Create new droplets
		for (int i = 0; i < max_new_droplets_per_iter; i++) {
			rand_add_droplet(0.1, p->width, p->height, 5, p);
		}
	}
	
	merge_overlapping_droplets(p);
	draw(p, draw_background_vao_id, droplet_shader_id, background_shader_id,
			window);
}


static void create_initial_droplets(Plane * p) {
	for (int i = 0; i < (p->width + p->height) / 4; i++) {
		Droplet new_droplet = droplet_create(p->width, p->height, 4.9);
		vector_add(&(p->droplets), new_droplet);
	}
}


static void create_testing_droplets(Plane * p, int scene_number) {
	/* Creates droplets in certain arrangements to help with testing */
	switch (scene_number) {
		case 1: {
			int const maxi = 5;
			int const maxj = 5;
			for (int i = 0; i < maxi; i++) {
				for (int j = 0; j < maxj; j++) {
					Droplet new_droplet;
					new_droplet.x = 50 + 60 * i;
					new_droplet.y = 50 + 50 * j;
					new_droplet.radius = j == 0 ? 6 : 3;
					vector_add(&(p->droplets), new_droplet);
				}
			}
			break;
		}
		case 2: {
			int const maxi = 5;
			int const maxj = 5;
			for (int i = 0; i < maxi; i++) {
				for (int j = 0; j < maxj; j++) {
					Droplet new_droplet;
					new_droplet.x = 50 + 60 * i;
					new_droplet.y = 50 + 50 * j;
					new_droplet.radius = (i + 1) * 2;
					vector_add(&(p->droplets), new_droplet);
				}
			}
			break;
		}
	}
}


int main(int argc, char * argv[]) {
	srand(time(NULL));
	
	// Setup SDL and GLEW
	SDL_Window * window = NULL;
	SDL_Renderer * renderer = NULL;
	SDL_GLContext gl_context;
	sdl_setup((int) PLANE_WIDTH, (int) PLANE_HEIGHT, &window, &renderer);
	gl_setup(&gl_context, window);
	
	// Get the directory of the executable
	source_dir = calloc(strlen(argv[0]) + 1, sizeof(argv[0]));
	strncpy(source_dir, argv[0], strlen(argv[0]));
	// Remove the filename from the path
	char * dir_end = strrchr(source_dir, '/');
	if (dir_end == NULL) {
		// Maybe a Windows path
		dir_end = strrchr(source_dir, '\\');
	}
	if (dir_end != NULL) {
		*dir_end = '\0';
	}
	
	/*--------------*/
	/* OpenGL setup */
	/****************/
	
	// Build the droplet shader program
	char * vertex_relative_path = get_relative_path(SHADER_DROPLET_VERTEX_FILE);
	char * fragment_relative_path = get_relative_path(
			SHADER_DROPLET_FRAGMENT_FILE);
	char * vertex_source = read_file(vertex_relative_path);
	char * fragment_source = read_file(fragment_relative_path);
	GLuint droplet_shader_id = create_gl_program(vertex_source,
			fragment_source);
	free(vertex_relative_path);
	free(fragment_relative_path);
	free(vertex_source);
	free(fragment_source);
	
	// Build the background shader program
	vertex_relative_path = get_relative_path(SHADER_BACKGROUND_VERTEX_FILE);
	fragment_relative_path = get_relative_path(SHADER_BACKGROUND_FRAGMENT_FILE);
	vertex_source = read_file(vertex_relative_path);
	fragment_source = read_file(fragment_relative_path);
	GLuint background_shader_id = create_gl_program(vertex_source,
			fragment_source);
	free(vertex_relative_path);
	free(fragment_relative_path);
	free(vertex_source);
	free(fragment_source);
	
	// Create the texture object for the background
	GLuint background_texture_id = render_background(background_shader_id);
	
	// Create the VAO for the background shader
	GLuint draw_background_buffer_ids[2];
	size_t draw_background_buffer_ids_length = \
			sizeof(draw_background_buffer_ids)
			/ sizeof(draw_background_buffer_ids[0]);
	GLuint draw_background_vao_id = create_draw_background_vao(
			background_texture_id, draw_background_buffer_ids,
			draw_background_buffer_ids_length);
	
	glClearColor(1.0, 1.0, 1.0, 1.0);
	
	/*^^^^^^^^^^^^^^^^^^^*/
	/* OpenGL setup done */
	/*-------------------*/
	
	Plane * p = plane_create(PLANE_WIDTH, PLANE_HEIGHT, PLANE_FRICTION);
	assert(p != NULL);
	
	if (USE_TEST_SCENE == 0) {
		create_initial_droplets(p);
	}
	else {
		create_testing_droplets(p, USE_TEST_SCENE);
	}
	int const max_new_droplets_per_iter = \
			imax(1, (int) ((p->width * p->height) / 200000));

	// Main loop
	double const min_frame_time_ms = MAX_FRAME_RATE != 0 \
			? (1.0 / MAX_FRAME_RATE) * 1000.0 \
			: 0;
	// Duration of the iteration excluding the sleep, in seconds
	double loop_time = 0.0;
	// Duration of the iteration including the sleep, in seconds
	double time_delta = 0.0;
	while (true) {
		long long start_time_usec = get_time_usec();
		loop(time_delta, max_new_droplets_per_iter, p, window,
				draw_background_vao_id, droplet_shader_id,
				background_shader_id);
		if (sdl_quit()) {
			break;
		}
		
		loop_time = (get_time_usec() - start_time_usec) / 1000000.0;
		// Sleep the necessary amount of time to not exceed the max frame rate
		int sleep_duration_ms = (int) (min_frame_time_ms - loop_time * 1000.0);
		if (MAX_FRAME_RATE > 0 && sleep_duration_ms > 0) {
			sleep_ms(sleep_duration_ms);
		}
		time_delta = (get_time_usec() - start_time_usec) / 1000000.0;
	}
	
	// Cleanup
	free(source_dir);
	plane_free(p);
	glDeleteTextures(1, &background_texture_id);
	glDeleteBuffers(draw_background_buffer_ids_length,
			draw_background_buffer_ids);
	glDeleteProgram(droplet_shader_id);
	glDeleteProgram(background_shader_id);
	glDeleteVertexArrays(1, &draw_background_vao_id);
	graphics_finish(window, renderer, &gl_context);
}
